package pers.xiaojun.boot.security.config;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import jakarta.annotation.security.PermitAll;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import pers.xiaojun.boot.security.config.properties.SecurityProperties;
import pers.xiaojun.boot.security.filter.TokenAuthenticationFilter;

import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 描述
 *
 * @author xiaojun
 * @since 2025-10-03
 */
@Slf4j
@AutoConfiguration
@AutoConfigureOrder(-1)
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true) // 启用方法级安全
@RequiredArgsConstructor
public class WebSecurityAutoConfiguration {

    private final AccessDeniedHandler accessDeniedHandler;

    private final AuthenticationEntryPoint authenticationEntryPoint;

    private final SecurityProperties securityProperties;

    private final TokenAuthenticationFilter tokenAuthenticationFilter;


    /**
     * 手动注册 AuthenticationManager 到Ioc中
     * 用户认证需要获得 AuthenticationManager 后写入当前认证信息
     *
     * @param authenticationConfiguration 认证配置
     * @return 认证管理器
     * @throws Exception 异常
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }


    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, RequestMappingHandlerMapping requestMapping) throws Exception {
        // 基础配置
        http
                // 开启 CORS 支持
                .cors(Customizer.withDefaults())
                // 禁用 csrf
                .csrf(AbstractHttpConfigurer::disable)
                // 禁用表单登录
                .formLogin(AbstractHttpConfigurer::disable)
                // 禁用 session（前后端分离用不到）
                .sessionManagement(c -> c.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // 关闭 X-Frame-Options 防护，允许页面使用<frame>等内嵌网页标签
                .headers(c -> c.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
                // 配置统一异常处理
                .exceptionHandling(auth -> auth
                        .accessDeniedHandler(accessDeniedHandler)
                        .authenticationEntryPoint(authenticationEntryPoint)
                )
        ;
        // 路由拦截策略
        // 获取PermitAll注解的所有路径
        Multimap<HttpMethod, String> permitAllPatternMap = getPermitAllPatternMap(requestMapping);
        http
                .authorizeHttpRequests(auth -> {
                            // 开放 @PermitAll 注解路径
                            permitAllPatternMap.asMap().forEach((httpMethod, patterns) -> {
                                auth.requestMatchers(httpMethod, patterns.toArray(new String[0])).permitAll();
                            });
                            // 静态资源可匿名访问
                            auth.requestMatchers(HttpMethod.GET, "/*.html", "/*.css", "/*.js", "/*.ico").permitAll();
                            // 自定义公开规则
                            auth.requestMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll();
                        }
                )
                // 其他所有请求都需要认证
                .authorizeHttpRequests(auth -> auth
                        .anyRequest().authenticated()
                );


        // 注册 Token 拦截器
        http.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();

    }

    /**
     * 获取所有接口方法带有 @PermitAll 注解的请求路径
     *
     * @return 请求方法映射请求路径（多值映射）
     */
    private Multimap<HttpMethod, String> getPermitAllPatternMap(RequestMappingHandlerMapping requestMapping) {
        // 所有请求接口
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMapping.getHandlerMethods();

        Multimap<HttpMethod, String> resultMap = HashMultimap.create();

        // RequestMappingInfo: 请求信息  HandlerMethod: 请求对应的方法
        handlerMethods.forEach((requestInfo, method) -> {
            // 方法上是否含有 @PermitAll 注解
            boolean isPermitAll = method.hasMethodAnnotation(PermitAll.class);
            // requestInfo.getPathPatternsCondition() 为新版获取方式 Spring 5.3+，
            // 旧版为 getPatternsCondition() 匹配 Ant风格路径
            if (isPermitAll && requestInfo.getPathPatternsCondition() != null) {
                // 获取当前请求方法 匹配的所有路径
                Set<String> patterns = requestInfo.getPathPatternsCondition().getPatternValues();
                // 获取当前请求方法 支持的所有请求方式
                Set<HttpMethod> methods = requestInfo.getMethodsCondition()
                        .getMethods().stream()
                        .map(RequestMethod::asHttpMethod)
                        .collect(Collectors.toSet());

                // 当请求方法没有设置具体的请求方式时，设置所有请求方式
                if (methods.isEmpty()) {
                    methods = Sets.newHashSet(
                            HttpMethod.GET,
                            HttpMethod.GET,
                            HttpMethod.DELETE,
                            HttpMethod.PUT,
                            HttpMethod.PATCH,
                            HttpMethod.HEAD
                    );
                }
                for (HttpMethod httpMethod : methods) {
                    resultMap.putAll(httpMethod, patterns);
                }

            }
        });

        return resultMap;
    }

}
